﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

namespace Pfz.DynamicObjects
{
	/// <summary>
	/// Class that implement interfaces built only of get/set properties
	/// making all the sets notifiable.
	/// </summary>
	public sealed class NotifyPropertyChangedGenerator
	{
		#region Inner Class - almost private
			/// <summary>
			/// This class is used internally, but it needs to be public as it is referenced
			/// directly by the generated code.
			/// </summary>
			[CLSCompliant(false)]
			public sealed class _Notifiable<T>:
				INotifyPropertyChanged
			{
				internal object _sender;

				internal void Notify(PropertyChangedEventArgs args)
				{
					var handler = PropertyChanged;
					if (handler != null)
						handler(_sender, args);
				}

				/// <summary>
				/// Implements the INotifyPropertyChanged interface.
				/// </summary>
				public event PropertyChangedEventHandler PropertyChanged;
			}
		#endregion

		#region MustBeCollectible
			/// <summary>
			/// Identifies if the generated assemblies will be collectible or not.
			/// </summary>
			public bool MustBeCollectible { get; set; }
		#endregion
		
		private readonly Dictionary<Type, Delegate> _creators = new Dictionary<Type, Delegate>();
		/// <summary>
		/// Gets a function that creates new objects of the given interface type.
		/// </summary>
		public Func<T> GetCreator<T>()
		{
			if (!typeof(T).IsInterface)
				throw new ArgumentException(typeof(T).FullName + " is not an interface type.");
		
			Delegate possibleResult;
			if (_creators.TryGetValue(typeof(T), out possibleResult))
				return (Func<T>)possibleResult;
				
			var typeBuilder = new DelegatedTypeBuilder<_Notifiable<T>>(null, MustBeCollectible, typeof(T), typeof(INotifyPropertyChanged));
			typeBuilder.AddEvent<PropertyChangedEventHandler>
			(
				"PropertyChanged",
				(notifiable, handler) => notifiable.PropertyChanged += handler,
				(notifiable, handler) => notifiable.PropertyChanged -= handler
			);
			
			_AddProperties(typeBuilder, typeof(T));
			foreach(var interfaceType in typeof(T).GetInterfaces())
				if (interfaceType != typeof(INotifyPropertyChanged))
					_AddProperties(typeBuilder, interfaceType);
					
			var baseCreator = typeBuilder.CreateCreator();
			
			var creator =
				new Func<T>
				(
					() =>
					{
						var userInstance = new _Notifiable<T>();
						var result = (T)baseCreator(userInstance);
						userInstance._sender = result;
						return result;
					}
				);
				
			_creators.Add(typeof(T), creator);
			return creator;
		}

		private static void _AddProperties<T>(DelegatedTypeBuilder<_Notifiable<T>> typeBuilder, Type type)
		{
			foreach(var property in type.GetProperties())
			{
				string name = property.Name;
				
				if (!property.CanRead || !property.CanWrite)
					throw new NotSupportedException("The NotifyPropertyChangedGenerator only works over properties with getter and setter. Property: " + name + ".");
					
				_AddProperty(typeBuilder, property);
			}
		}

		private static readonly MethodInfo _AddProperty2Method = typeof(NotifyPropertyChangedGenerator).GetMethod("_AddProperty2", BindingFlags.NonPublic | BindingFlags.Static);
		private static void _AddProperty<TInterface>(DelegatedTypeBuilder<_Notifiable<TInterface>> typeBuilder, PropertyInfo property)
		{
			var method = _AddProperty2Method.MakeGenericMethod(typeof(TInterface), property.PropertyType);
			method.Invoke(null, new object[]{typeBuilder, property});
		}
		private static void _AddProperty2<TInterface, TProperty>(DelegatedTypeBuilder<_Notifiable<TInterface>> typeBuilder, PropertyInfo property)
		{
			string name = property.Name;
			var args = new PropertyChangedEventArgs(name);
			
			typeBuilder.AddPropertyWithField<TProperty>
				(
					name,
					null,
					(_Notifiable<TInterface> userInstance, ref TProperty fieldValue, TProperty newValue) =>
					{
						if (EqualityComparer<TProperty>.Default.Equals(newValue, fieldValue))
							return;
							
						fieldValue = newValue;
						userInstance.Notify(args);
					},
					null
				);
		}
	}
}
